CRM 销售机会信息管理(三)

3.8 - 线索跟踪

在用户提交了个人信息后,后台产生了线索数据,接下来我们需要对线索进行跟踪和处理。在本任务中,我们需要完成对线索的编辑还有为其添加跟踪记录。在线索的编辑页面,我们通过线索 ID 获取到线索的内容,在页面展示初始化的数据。通过编辑选择用户的意向状态,以及分配给不同的销售。同时在线索列表中,需要展示该分配的销售名称。

新建线索记录 model

/models/log.js

1
2
3
4
5
6
7
8
9
const Base = require('./base.js');

class ClueLog extends Base {
constructor(props = 'clue_log') {
super(props);
}
}

module.exports = new ClueLog()

在控制器中添加页面渲染、修改、添加记录的方法

/controllers/clue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
// ...
const ClueLog = require('./../models/log.js');
const User = require('./../models/user.js');

const userController = {
...,
log: async function(req,res,next) {
try{
const id = req.params.id;
const clues = await Clue.select({ id })
const logs = await ClueLog.select({ clue_id : id})
const users = await User.select({ role: 2 })
res.locals.users = users.map(data => {
return {
id: data.id,
name: data.name
}
});
res.locals.clue = clues[0]
res.locals.clue.created_time_display = formatTime(res.locals.clue.created_time);
res.locals.logs = logs.map((data)=>{
data.created_time_display = formatTime(data.created_time);
return data
});
res.render('admin/clue_log.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals);
}
},
update: async function(req,res,next) {
let status = req.body.status;
let remark = req.body.remark;
let id = req.params.id;
let user_id = req.body.user_id;
if(!status || !remark){
res.json({ code: 0, message: '缺少必要参数' });
return
}

try{
const clue = await Clue.update( id ,{
status, remark, user_id
});
res.json({
code: 200,
data: clue
})
}catch(e){
console.log(e)
res.json({
code: 0,
message: '内部错误'
})
}
},
addLog: async function(req,res,next){
let content = req.body.content;
let created_time = new Date();
let clue_id = req.params.id;
if(!content){
res.json({ code: 0, message: '缺少必要参数' });
return
}

try{
const clue = await ClueLog.insert({
content, created_time, clue_id
});
res.json({
code: 200,
data: clue
})
}catch(e){
console.log(e)
res.json({
code: 0,
message: '内部错误'
})
}
}
}

添加、修改相关路由

/routes/index.js

1
2
3
4
// router.get('/admin/clue/:id', function(req, res, next) {
// res.render('admin/clue_log');
// });
router.get('/admin/clue/:id', clueController.log);

routes/api.js

1
2
3
4
//...
router.put('/clue/:id' , clueController.update);
router.post('/clue/:id/log', clueController.addLog);
//...

修改记录模版,渲染默认数据及创建和引入脚本文件。

/views/admin/clue_log.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
{% extends './../admin_layout.tpl' %}

{% block content %}
<div class="content-title">跟踪线索</div>
<div class="content-control">
<a href="/admin/clue">返回线索列表 >></a>
</div>
<div class="content-mainer">
<div class="form-section">
<div class="form-item">
<span class="form-text">客户名称:{{clue.name}}</span>
</div>
<div class="form-item">
<span class="form-text">联系电话:{{clue.phone}}</span>
</div>
<div class="form-item">
<span class="form-text">线索来源:{{clue.utm}}</span>
</div>
<div class="form-item">
<span class="form-text">创建时间:{{clue.created_time_display}}</span>
</div>
<div class="form-item">
<span class="form-text">用户状态:</span>
<div class="form-item">
<select id="clueStatus" class="form-input">
<option value="0">请选择线索状态</option>
<option value="1" {% if clue.status == 1 %} selected {% endif %}>没有意向</option>
<option value="2" {% if clue.status == 2 %} selected {% endif %}>意向一般</option>
<option value="3" {% if clue.status == 3 %} selected {% endif %}>意向强烈</option>
<option value="4" {% if clue.status == 4 %} selected {% endif %}>完成销售</option>
<option value="5" {% if clue.status == 5 %} selected {% endif %}>取消销售</option>
</select>
</div>
</div>
<div class="form-item">
<span class="form-text">当前分配销售:</span>
<div class="form-item">
<select id="clueUserId" class="form-input">
<option value="0">请选择分配销售</option>
{% for val in users %}
<option value="{{val.id}}" {% if clue.user_id == val.id %} selected {% endif %}>{{val.name}}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-item">
<p class="form-text">备注:</p>
<textarea id="clueRemark" class="form-textarea" placeholder="备注信息">{{clue.remark}}</textarea>
</div>
<div class="form-item">
<input id="clueId" type="text" hidden value="{{clue.id}}" />
<button id="clueSubmit" class="form-button">保存</button>
</div>
</div>
<div class="log-section">
<ul class="log-list">
{% for val in logs %}
<li class="log-item">
<p class="log-data">{{val.created_time_display}}</p>
<p class="log-content">{{val.content}}</p>
</li>
{% else %}
<li class="log-item">
<p class="log-content">当前没有记录</p>
</li>
{% endfor %}
</ul>
<div class="form-section">
<div class="form-item">
<p class="form-text">添加记录:</p>
<textarea id="logContent" class="form-textarea" placeholder="请输入本次跟踪的记录 ~"></textarea>
</div>
<div class="form-item">
<button id="logSubmit" class="form-button">添加</button>
</div>
</div>
</div>
</div>
{% endblock %}

{% block js %}
<script src="/javascripts/jquery-3.3.1.min.js"></script>
<script src="/javascripts/clue_log.js"></script>
{% endblock %}

/public/javascripts/clue_log.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const PAGE = {
init: function() {
this.bind();
},
bind: function() {
$('#clueSubmit').bind('click',this.handleEditClueSubmit);
$('#logSubmit').bind('click',this.handleAddClueLog);
},
handleEditClueSubmit: function() {
let id = $('#clueId').val();
let status = $('#clueStatus').val();
let remark = $('#clueRemark').val();
let user_id = $('#clueUserId').val();
status = Number(status)
if(!id || !status || !remark || !user_id){
alert('请输入必要参数');
return
}

$.ajax({
url: '/api/clue/' + id,
data: { remark, status, user_id },
type: 'PUT',
beforeSend: function() {
$("#clueSubmit").attr("disabled",true);
},
success: function(data) {
if(data.code === 200){
alert('编辑成功!')
}else{
alert(data.message)
}
},
error: function(err) {
console.log(err)
},
complete: function() {
$("#clueSubmit").attr("disabled",false);
}
})
},
handleAddClueLog: function() {
let content = $('#logContent').val();
let id = $('#clueId').val();
if(!content){
alert('请输入必要参数');
return
}

$.ajax({
url: '/api/clue/' + id +'/log',
data: { content },
type: 'POST',
beforeSend: function() {
$("#logSubmit").attr("disabled",true);
},
success: function(data) {
if(data.code === 200){
alert('添加成功!')
location.reload();
}else{
alert(data.message)
}
},
error: function(err) {
console.log(err)
},
complete: function() {
$("#logSubmit").attr("disabled",false);
}
})
}
}

PAGE.init();

3.9 - 销售展示

在当前的项目中,我们已经完成了所有数据的流程。用户可以增加和修改,而且可以登录和退出。在落地页中提交的数据在线索管理中可以被编辑、分配和跟踪。但当前我们也发现一个问题,管理员和销售拥有相同的权限,他们可以看相同的页面。因此本任务中,我们需要区分管理员和销售他们所拥有的数据展现,管理员可以操作添加数据,且可以编辑及把线索分配给销售。销售的角色在本项目中,仅可登录且对分配给自己的线索进行展示和跟踪。

切换到管理员身份登录,或者去用户管理页面修改当前账号身份。

使用联表查询,修改线索展示中销售的 id 改为销售名字,并更具返回对应销售角色的数据。

models/clue.js
使用leftjoin方法,否则就会出现当数据库表的某个位置为null时,数据无法加载到页面上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Base = require('./base.js');
const knex = require('./knex');

class Clue extends Base {
constructor(props = 'clue') {
super(props);
}
joinUser(params={}){
return knex('clue')
left.join('user', 'clue.user_id', '=', 'user.id')//leftjoin()加不加点也可以用
.select(
'clue.id',
'clue.name',
'clue.phone',
'clue.utm',
'clue.status',
'clue.created_time',
{'sales_name': 'user.name'},
).where(params)
}

}
module.exports = new Clue()

controllers/clue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const userController = {
...,
show: async function(req,res,next){
try{
//start...
const role = res.locals.userInfo.role;
const user_id = res.locals.userInfo.id;
let params = {};
if (role == 2) {
params.user_id = user_id
}
const clues = await Clue.joinUser(params);
//end...
res.locals.clues = clues.map((data)=>{
data.created_time_display = formatTime(data.created_time);
return data
});
res.render('admin/clue.tpl',res.locals)
}catch(e){
res.locals.error = e;
res.render('error',res.locals);
}
},
...
}

/views/admin/clue.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{% extends './../admin_layout.tpl' %}

{% block content %}
<div class="content-title">线索管理</div>
<div class="content-table">
<table class="table-container">
<tr>
<th>姓名</th>
<th>电话</th>
<th>来源</th>
<th>创建时间</th>
<th>跟踪销售</th>
<th>状态</th>
<th>操作</th>
</tr>
{% for val in clues %}
<tr>
<td>{{ val.name }}</td>
<td>{{ val.phone }}</td>
<td>{{ val.utm }}</td>
<td>{{ val.created_time_display }}</td>
<td>{{ val.sales_name }}</td>
{% if val.status == 1 %}
<td>没有意向</td>
{% elif val.status == 2 %}
<td>意向一般</td>
{% elif val.status == 3 %}
<td>意向强烈</td>
{% elif val.status == 4 %}
<td>完成销售</td>
{% elif val.status == 5 %}
<td>取消销售</td>
{% else %}
<td>-</td>
{% endif %}
<td><a href="/admin/clue/{{val.id}}">跟踪</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}

在线索记录页面中,对于销售角色隐藏编辑功能。
views/admin/clue_log.tpl

1
2
3
4
5
6
7
8
9
<!-- ... -->
<div class="content-mainer">
{% if userInfo.role == 1 %}
<div class="form-section">
...
</div>
{% endif %}
</div>
<!-- ... -->

人员管理页面中,仅有管理员可看。如果销售访问,禁止显示。
/views/admin_layout.tpl

1
2
3
4
5
{% if userInfo.role == 1 %}
<li>
<a class="page-nav-item" href="/admin/user">人员管理</a>
</li>
{% endif %}

/middlewares/auth.tpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const authMiddleware = {
mustLogin: function(req,res,next){
if(!res.locals.isLogin){
res.redirect('/admin/login')
return
}

next();
},
mustRoot: function(req,res,next){
if(res.locals.userInfo.role != 1){
res.writeHead(403);
res.end("403 Forbidden");
return
}
next();
}
}

module.exports = authMiddleware;

再修稿路由即可

1
2
3
4
5
6
7
router.get('/admin/login', authController.renderLogin);
// router.get('/admin/user',authMiddleware.mustLogin, userController.show);//authMiddleware.mustLogin,
// router.get('/admin/user/create', authMiddleware.mustLogin,userController.renderUserCreate);//
// router.get('/admin/user/:id/edit',authMiddleware.mustLogin, userController.edit);//
router.get('/admin/user', authMiddleware.mustLogin, authMiddleware.mustRoot, userController.show);
router.get('/admin/user/create', authMiddleware.mustLogin, authMiddleware.mustRoot, userController.renderUserCreate);
router.get('/admin/user/:id/edit', authMiddleware.mustLogin, authMiddleware.mustRoot, userController.edit);

把公共样式的登录名修改
view/admin_layou.tpl

1
2
3
4
5
6
7
<!-- <div class="head-name">林熙</div> -->
{% if userInfo.role == 1 %}
<span class="head-name" value="{{userInfo.role}}">{{userInfo.name}},您好!管理员</span>
{% else %}
<span class="head-name" value="{{userInfo.role}}">{{userInfo.name}},您好,欢迎您</span>
{% endif %}
<a href="/admin/outlogin" id="head-quit">退出</a>

在设置cookie时添加用户名的操作
contlloers/auth.js

1
2
3
4
···
res.cookie('ac', auth_Code, { maxAge: 24* 60 * 60 * 1000, httpOnly: true });
res.cookie('user_name',user.name,{maxAge:24*60*60*1000,httpOnly:true})//这个位置
···

在中间层添加入名字

1
2
3
4
5
6
7
8
9
10
11
12
13
const authCodeFunc = require('./../utils/authCode.js');
module.exports = function(req,res,next){
···
let auth_Code = req.cookies.ac;
let name = req.cookies.user_name;//这里
if(auth_Code){
···
res.locals.userInfo = {
phone,password,id,role,name,
}
}
next();
}

完成图



好了,所有步骤已完成